﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using ServiceStack.Redis;
using PServiceBus.Core.Runtime;
using PServiceBus.Core.Logger;
using System.Threading.Tasks;
using System.Collections;
using PServiceBus.Core.Runtime.Extensions;

namespace PServiceBus.Redis {
    public abstract class RedisCollection<TObject> : IDisposable, IEnumerable<TObject> where TObject : class {

        private object _lockQueryObject = new object();
        private bool _isDisposed;
        private const string REDIS_PERSIST_KEY = "QueryPersist";
        protected const string READ_ONLY_KEY = "ReadOnly";

        private ConcurrentDictionary<string, RedisNativeClient> _clients = new ConcurrentDictionary<string, RedisNativeClient>();

        public string Endpoint { get; set; }

        public string EndpointAlias { get; set; }

        public bool AppendTypeNameToEndpoint { get; set; }

        public Func<TObject, Guid> IDFunc { get; set; }

        protected byte[] GetID(TObject obj) {
            return IDFunc(obj).ToByteArray();
        }

        private EndpointUri GetEndpointUri() {
            var endpoint = Endpoint;
            var path = AppendTypeNameToEndpoint ? typeof(TObject).FullName.Replace(".", "_") : string.Empty;
            path += (EndpointAlias ?? string.Empty);
            endpoint += (endpoint.EndsWith("/") ? string.Empty : "/") + path;
            return EndpointUri.Parse(endpoint);
        }

        private RedisNativeClient GetRedisConnection() {
            var info = RedisInfo;
            var client = new RedisNativeClient(info.Host, info.Port);
            if (!String.IsNullOrWhiteSpace(info.Password))
                client.Password = info.Password;
            return client;
        }

        protected RedisNativeClient GetConnection(string name) {
            var client = default(RedisNativeClient);
            var info = RedisInfo;
            if (!_clients.TryGetValue(name, out client)) {
                client = _clients[name] = GetRedisConnection();
            }
            if (client.HadExceptions) {
                MethodHelper.Try(() => client.Dispose());
                client = _clients[name] = GetRedisConnection();
            }
            return client;
        }


        protected void Query(Action<RedisNativeClient> action) {
            try {
                lock (_lockQueryObject) {
                    var client = GetConnection(REDIS_PERSIST_KEY);
                    action(client);
                }
            } catch (Exception ex) {
                ESBLogger.Log(ex);
            }
        }

        protected TReturn Query<TReturn>(Func<RedisNativeClient, TReturn> func) {
            var result = default(TReturn);
            try {
                lock (_lockQueryObject) {
                    result = func(GetConnection(REDIS_PERSIST_KEY));
                }
            } catch (Exception ex) {
                ESBLogger.Log(ex);
            }
            return result;
        }

        public ulong Incr(string key) {
            return Query(client => (ulong)client.Incr(key));
        }

        public ulong Decr(string key) {
            return Query(client => (ulong)client.Decr(key));
        }

        public TValue Get<TValue>(string key) {
            return Query(client =>
            {
                var buffer = client.Get(key);
                return (TValue)Convert.ChangeType(buffer == null ? "0" : buffer.UTF8ByteToString(),
                    typeof(TValue));
            });
        }

        public void Set<TValue>(string key, TValue value) {
            Query(client => client.Set(key, value.ToString().ToUTF8Bytes()));
        }

        private DateTime _lastSave = DateTime.Now;
        private void SaveAsync() {
            var client = GetConnection(REDIS_PERSIST_KEY);
            MethodHelper.Try(() =>
            {
                var lastSave = client.LastSave;
                if (lastSave > _lastSave) {
                    Task.Factory.StartNew(o =>
                    {
                        var redis = o as RedisNativeClient;
                        MethodHelper.Try(() => redis.BgSave());
                    }, client);
                    _lastSave = lastSave;
                }
            });
        }

        public void SaveSync() {
            var client = GetConnection(REDIS_PERSIST_KEY);
            client.Save();
        }

        protected string Key { get { return RedisInfo.Path; } }

        private EndpointUri _endpointUri = null;
        protected EndpointUri RedisInfo {
            get {
                return _endpointUri ?? (_endpointUri = GetEndpointUri());
            }
        }

        public void Clear() {
            Query(client =>
            {
                client.Del(Key);
            });
        }

        public void Delete() {
            Clear();
        }

        public abstract IEnumerator<TObject> GetObjects();

        public abstract bool Add(IEnumerable<TObject> list);
        public abstract bool Add(TObject obj);

        public abstract bool Delete(IEnumerable<Func<TObject, bool>> conditions);
        public abstract bool Delete(IEnumerable<TObject> list);
        public abstract bool Delete(Func<TObject, bool> condition);
        public abstract bool Delete(TObject obj);
        public abstract bool Exists(Func<TObject, bool> condition);
        public abstract bool Exists(TObject obj);
        public abstract TObject Get(Func<TObject, bool> condition);
        public abstract TObject Get(Guid id);
        public abstract bool Refresh(TObject obj);

        #region IDisposable Members

        public void Dispose() {
            if (_isDisposed) return;
            foreach (var kv in _clients) {
                var client = kv.Value;
                if (!client.HadExceptions)
                    MethodHelper.Try(client.Dispose);
            }
            _clients.Clear();
            _isDisposed = true;
        }

        #endregion

        #region IEnumerable<TObject> Members

        public IEnumerator<TObject> GetEnumerator() {
            return GetObjects();
        }

        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator() {
            return GetObjects();
        }

        #endregion
    }
}
